home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Nebula 2
/
Nebula Two.iso
/
SourceCode
/
GameKit
/
gamekit-1
/
HighScoreServer.m
< prev
next >
Wrap
Text File
|
1995-06-12
|
17KB
|
518 lines
// HighScoreServer.m
// under development; anything with ***** is yet to be completed
// This class is the actual high score server for any given game.
// You shouldn't need to subclass it: Just change the class of
// HighScoreSlot used and change the GameInfo object's parameters.
// I need to do exception handling in here to catch any DO problems,
// but haven't got around to it yet, so exceptions will currently crash
// the server. :-< *****
// Note that a game with multiple tables simply uses multiple servers,
// each with a different name. (For example: NX_Invaders.easy,
// NX_Invaders.hard would be a possibility. The HighScoreController in
// the game itself handles all the coordination between tables; by
// default they are numbered.)
// In the future it would be nice to allow the game to send a .o file
// over the connection for any high score slots the server can't deal
// with. This would allow the server to truly dynamically update itself!
// Right now, though, there's not an easy way to do this and I don't have
// the time to take care of it, since other parts of the GameKit need more
// attention. The method frameworks are in the protocol, though.
#import <appkit/appkit.h>
#import <daymisckit/daymisckit.h>
#import <gamekit/gamekit.h>
#import <remote/NXProxy.h>
#import <objc/objc-runtime.h>
#import <objc/objc-load.h>
// this is where the files are stored. Each file is named
// using the convention "<game's name>.highscores" The files
// themselves are all typed streams with archived GameInfo and
// HighScoreTable objects in them. Override the -pathToTables
// method to change this.
#define PATH_TO_TABLES "/usr/local/games/highscores/"
static BOOL loggingIsOn; // a flag to turn all logging on or
// off for all servers in an application
static id logFile;
// define DEBUGLOG for even more logging...
@implementation HighScoreServer
+ turnLoggingOn:(BOOL)flag
{
loggingIsOn = flag;
if (loggingIsOn) { // make new log file when turning on logging
// (This allows us to change the name of the file by toggling
// the on and off. A subclass would have to implement +makeLogFile
// in order to actually do something like this.)
if (logFile) [logFile free];
logFile = [self makeLogFile];
}
return self;
}
+ makeLogFile // override to change where loggin goes to.
{ // Just return a DAYLogFile instance!
id fileName = [[DAYString newWithString:PATH_TO_TABLES] cat:"log"];
// I don't use an attendant lock file since there should only
// ever be one server after the file -- note that I'm intending
// this facility for remote servers, not local. If you are going
// to do logging from a local game, you _better_ give it a lock file!!!
id newLog = [[[DAYLogFile alloc] init] setFileName:fileName];
return newLog;
}
- init { return [self error:"Use -initForGame: (not -init)"]; }
- initForGame:(const char *)name // designated initializer
{
[super init];
if (!name) { // must give us a name or we'll barf.
#ifdef DEBUGLOG
if (loggingIsOn) {
id tempString = [DAYString
newWithString:"An attempt was made to start a NULL server."];
[logFile addLineToLogFile:tempString];
[tempString free];
}
#endif
fprintf(stderr, "HighScoreServer error: can't init for null game.\n");
[self free];
return nil; // note that nothing is returned...
}
// find file that holds the high score file
// if non-existent, create an empty table, otherwise we load it in
gameName = [[DAYString alloc] initString:name];
if (loggingIsOn) {
id tempString = [[gameName copy] cat:": Starting server.\n"];
[logFile addLineToLogFile:tempString];
[tempString free];
}
clientList = [[List alloc] initCount:0];
connList = [[List alloc] initCount:0];
clientAuth = [[Storage alloc]
initCount:0 elementSize:sizeof(char) description:"c"];
[[[GameInfo alloc] init] free]; // forces class to be linked into serverd
// without the need for the ld flag that links in the whole library...
gameInfo = nil; // we'll force the first client to check in to send it
// build the name of the file where we store the highscores
scoreFile = [[DAYString alloc] initString:[self pathToTables]];
[scoreFile cat:"/"];
[scoreFile concatenate:gameName];
[scoreFile cat:".highscores"];
[self load]; // load in a highscore table
return self;
}
- (const char *)pathToTables
{ // subclass can override this to customize.
return PATH_TO_TABLES;
}
- (oneway)addSlotCode:(bycopy in id)code // not yet implemented
{ // *****
if (loggingIsOn) {
id tempString = [[gameName copy]
cat:": Slot code sent to server.\n"];
[logFile addLineToLogFile:tempString];
[tempString free];
}
return self;
}
- (oneway)setGameInfo:(bycopy in id)info
{
id emptySlotClass;
const char *slotClass = [[info slotType] stringValue];
// avoid freeing GameInfo if in a game and not a remote server
if (![NXApp delegate]) if (gameInfo) [gameInfo free];
gameInfo = info; // we now have a gameInfo object that tells us
// how to do things. Without this, we can only function according
// to the gamekit defaults.
emptySlotClass = objc_lookUpClass(slotClass);
if (!emptySlotClass) { // can't find the class, so try and load it
long ret;
char *fileName = (char *)malloc(strlen(slotClass) +
strlen([self pathToTables]) + 3);
char *fileNames[2] = {fileName, NULL};
sprintf(fileName, "%s%s.o", [self pathToTables], slotClass);
ret = objc_loadModules(fileNames, NULL, NULL, NULL, NULL);
emptySlotClass = objc_lookUpClass(slotClass);
if (ret || !emptySlotClass) {
// couldn't load the class so tell the clients we can't help.
[clientList makeObjectsPerform:@selector(cantBeServed:)
with:gameName];
if (![NXApp delegate]) [gameInfo free];
gameInfo = nil; // assume we're still uninitted.
return self;
}
}
[table setEmptySlotClass:emptySlotClass];
[table setMaxHighScores:[gameInfo maxHighScores]];
// ***** need to figure out which table we are so that this works right
[table setMaxScoresPerPlayer:[gameInfo maxScoresPerPlayerTable:0 net:YES]];
#ifdef NOISYDEBUG
fprintf(stderr, "HighScoreServer: maxScorePerPlayer is %d.\n",
[gameInfo maxScoresPerPlayerTable:0 net:YES]);
#endif
if (loggingIsOn) {
id tempString = [[gameName copy] cat:": Got GameInfo object.\n"];
[logFile addLineToLogFile:tempString];
[gameInfo dumpToLog:logFile];
[tempString free];
}
[self save]; // so even if no slot is sent we at least have GameInfo saved.
return self;
}
- (oneway)setTemplate:(bycopy in id)newTemplate
{
if (template) [template free];
template = newTemplate;
if (haveNonTemplateTable) {
int i;
haveNonTemplateTable = NO;
[self _makeTableRatherThanLoad];
for (i=0; i<[clientList count]; i++)
[[clientList objectAt:i] acceptTable:table name:gameName];
}
if (loggingIsOn) {
id tempString = [[gameName copy]
cat:": Got table template object.\n"];
[logFile addLineToLogFile:tempString];
[tempString free];
}
return self;
}
- free
{
// free our private strings
[scoreFile free];
[gameName free];
// free all high score slots and tables
[[table freeObjects] free];
// free other items (internal params)
[clientList free];
[clientAuth free];
[connList free];
if (![NXApp delegate]) [gameInfo free];
return [super free];
}
// methods that the client can call
- (oneway)clientDying:(in id <HighScoreClient>)client
// alerts server that a client is going away
{
// remove client from the list
unsigned num = [clientList indexOf:client];
while (num != NX_NOT_IN_LIST) {
[clientList removeObject:client];
[connList removeObjectAt:num];
[clientAuth removeElementAt:num];
num = [clientList indexOf:client];
}
#ifdef DEBUGLOG
if (loggingIsOn) {
id tempString = [[gameName copy] cat:": A client left.\n"];
[logFile addLineToLogFile:tempString];
[gameInfo dumpToLog:logFile];
[tempString free];
}
#endif
return self;
}
- senderIsInvalid:sender
{
int i;
BOOL changed = YES;
while (changed) {
changed = NO;
for (i=0; i<[connList count]; i++) {
if (sender == [connList objectAt:i]) {
[clientList removeObjectAt:i];
[connList removeObjectAt:i];
[clientAuth removeElementAt:i];
changed = YES;
break; // for loop -- have to restart iteration, since
// list object has been changed now, but we also
// want to be sure that we remove multiple pointers
// to dead clients if they exist, hence the while loop
} } }
#ifdef DEBUGLOG
if (loggingIsOn) {
id tempString = [[gameName copy] cat:": A client died.\n"];
[logFile addLineToLogFile:tempString];
[gameInfo dumpToLog:logFile];
[tempString free];
}
#endif
return self;
}
- (oneway)clientCheckIn:(in id <HighScoreClient>)client
// new client alerts of presence so that server
// can notify client of changes in the table
{
BOOL *auth = (BOOL *)malloc(sizeof(BOOL));
NXConnection *conn;
*auth = NO;
if ([(NXProxy *)client isProxy]) // could be Object subclass, too.
conn = [(NXProxy *)client connectionForProxy];
else { // client is local, so no connection, and it's authorized.
conn = nil;
*auth = YES;
}
// add the client to the list
[clientList addObject:client];
[connList addObject:conn];
[clientAuth addElement:auth];
[conn registerForInvalidationNotification:self];
#ifdef DEBUGLOG
if (loggingIsOn) {
id tempString = [[gameName copy] cat:": A client checked in.\n"];
[logFile addLineToLogFile:tempString];
[gameInfo dumpToLog:logFile];
[tempString free];
}
#endif
// ask for gameInfo object if we don't have it yet
if (!gameInfo) [client sendGameInfoTo:gameName];
else [client acceptTable:table name:gameName];
return self;
}
- (oneway)addSlot:newSlot // new high scores come in here
fromClient:(in id <HighScoreClient>)client // and go to all clients
{
int c;
id <HighScoreClient> tempClient; // gets rid of protocol warnings
if (loggingIsOn) {
id tempString = [[gameName copy] cat:": A new slot was submitted.\n"];
[logFile addLineToLogFile:tempString];
[newSlot dumpToLog:logFile];
[tempString free];
}
// insert the new score into the table; return if it doesn't fit
if (![table addSlot:newSlot]) return self;
// The server will send each client -addSlot:tableName: messages for every
// slot which changes while the client is connected. That way, the client
// can update panels, etc. when someone else gets a new highscore.
for (c=0; c<[clientList count]; c++) {
tempClient = [clientList objectAt:c];
if (client != tempClient) {
[tempClient addSlot:newSlot tableName:gameName];
}
}
// save the table to a file. Done every time there's a change
// so that nothing is lost if we crash, die, or get killed.
[self save];
return self;
}
- (BOOL)authorize:(id <HighScoreClient>)client
{ // get password from client and compare to gameInfo password via crypt()
id password = [client password]; // returns a DAYString
id encr = [password encrypt:[[gameInfo encryptedPassword] left:2]];
BOOL ret = YES;
if ([encr compareTo:[gameInfo encryptedPassword]]) ret = NO;
if (loggingIsOn) {
id tempString = [[gameName copy] cat:": Client sent password "];
if (ret) [tempString cat:"successfully.\n"];
else [tempString cat:"but it was wrong.\n"];
[logFile addLineToLogFile:tempString];
[tempString free];
}
return ret;
}
- (BOOL)validateClient:(id <HighScoreClient>)client
{
unsigned num = [clientList indexOf:client];
BOOL *valid = [clientAuth elementAt:num];
if (*valid != YES) return [self authorize:client];
return YES;
}
- (oneway)clearTable:(in id <HighScoreClient>)sender
// zero out the table. Asks sender for
// proper authentication first. See the table
// editing app for an example of how to do this.
{ // need to validate sender first off
int c; id <HighScoreClient> tempClient; // gets rid of protocol warnings
if (![self validateClient:sender]) return self;
if (loggingIsOn) {
id tempString = [[gameName copy] cat:": Client cleared the table.\n"];
[logFile addLineToLogFile:tempString];
[tempString free];
}
// clear the table
[table freeObjects];
if (template) { // if available, use the empty "template"
[table free];
table = [template copy];
}
[self save];
// now send it off to the clients
for (c=0; c<[clientList count]; c++) {
tempClient = [clientList objectAt:c];
// ***** commented out since we want to update _all_ clients!
//if (sender != tempClient) {
[tempClient acceptTable:table name:gameName];
//}
}
return self;
}
- (oneway)deleteSlot:(in int)i client:(in id <HighScoreClient>)sender
{
id <HighScoreClient> tempClient; // gets rid of protocol warnings
int c;
if (![self validateClient:sender]) return self;
if (loggingIsOn) {
char *string = (char *)malloc(16);
id tempString = [[gameName copy] cat:": Client removed slot #"];
sprintf(string, "%d\n", i);
[tempString cat:string];
[logFile addLineToLogFile:tempString];
[tempString free];
free(string);
}
[table removeObjectAt:i];
for (c=0; c<[clientList count]; c++) {
tempClient = [clientList objectAt:c];
if (sender != [clientList objectAt:c]) {
[tempClient removeSlotAt:i tableName:gameName];
}
}
[self save];
return self;
}
- (oneway)replaceSlot:(in int)i with:(bycopy in id)aSlot
client:(in id <HighScoreClient>)sender
{
int c; id <HighScoreClient> tempClient; // gets rid of protocol warnings
if (![self validateClient:sender]) return self;
if (loggingIsOn) {
char *string = (char *)malloc(16);
id tempString = [[gameName copy] cat:": Client replaced slot #"];
sprintf(string, "%d\n", i);
[tempString cat:string];
[logFile addLineToLogFile:tempString];
[tempString free];
free(string);
}
[table replaceObjectAt:i with:aSlot];
for (c=0; c<[clientList count]; c++) {
tempClient = [clientList objectAt:c];
if (sender != tempClient) {
[tempClient replaceSlotAt:i with:aSlot tableName:gameName];
}
}
[self save];
return self;
}
// ***** not yet implemented. Will be used mostly by editors to guarantee
// edits properly sent to all clients. For locking all other clients out
// of a table temporarily...
- (oneway)lockTable { return self; }
- (oneway)unlockTable { return self; }
- (const char *)gameName // Name of the game we are serving
{ return [gameName stringValue]; }
- save // flushes the table to the appropriate file.
{
NXTypedStream *typedStream;
haveNonTemplateTable = NO;
NX_DURING
typedStream = NXOpenTypedStreamForFile([scoreFile stringValue],
NX_WRITEONLY);
NXWriteObject(typedStream, gameInfo);
NXWriteObject(typedStream, table);
NXWriteObject(typedStream, template);
NXCloseTypedStream(typedStream);
NX_HANDLER
// deal with typed stream errors here *****
fprintf(stderr, "Exception %d raised in -save.\n",
NXLocalHandler.code);
NX_ENDHANDLER
#ifdef DEBUGLOG
if (loggingIsOn) {
id tempString = [[gameName copy] cat:": Saved table.\n"];
[logFile addLineToLogFile:tempString];
[tempString free];
}
#endif
return self;
}
- _makeTableRatherThanLoad
{
table = [template copy];
if (!table) {
haveNonTemplateTable = YES;
table = [[HighScoreTable alloc] init]; // ***** should follow gameInfo params...
}
#ifdef DEBUGLOG
if (loggingIsOn) {
id tempString = [[gameName copy]
cat:": Built table from template.\n"];
[logFile addLineToLogFile:tempString];
[tempString free];
}
#endif
return self;
}
- load // load the table from a file, if it exists.
{
NXTypedStream *typedStream;
FILE *testFile;
haveNonTemplateTable = NO;
// for some reason, NXOpenTypedStreamForFile() isn't returning
// NULL for me when the file doesn't exist, so I check for the
// file's existence first.
testFile = fopen([scoreFile stringValue], "r");
if (!testFile) return [self _makeTableRatherThanLoad];
fclose(testFile);
NX_DURING
typedStream = NXOpenTypedStreamForFile([scoreFile stringValue],
NX_READONLY);
if (!typedStream) return [self _makeTableRatherThanLoad];
if (table) [[table freeObjects] free];
if (template) [[template freeObjects] free];
gameInfo = NXReadObject(typedStream);
// ***** should load dynamic classes here; GameInfo might require it!
table = NXReadObject(typedStream);
template = NXReadObject(typedStream);
NXCloseTypedStream(typedStream);
NX_HANDLER
// deal with typed stream errors here *****
[self _makeTableRatherThanLoad];
fprintf(stderr, "Exception %d raised in -load.\n",
NXLocalHandler.code);
NX_ENDHANDLER
#ifdef DEBUGLOG
if (loggingIsOn) {
id tempString = [[gameName copy] cat:": Loaded table.\n"];
[logFile addLineToLogFile:tempString];
[tempString free];
}
#endif
return self;
}
@end